<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
  Closure Unit Tests - goog.messaging.PortChannel
</title>
<script src="../base.js"></script>
<script>
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.messaging.PortChannel');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.async.MockControl');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.messaging.MockMessageEvent');
goog.require('goog.userAgent.product');
</script>
</head>
<body>
<div id="frame"></div>
<script>

var mockControl;
var asyncMockControl;
var mockPort;
var portChannel;

var workerChannel;
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var timer;

// Use a relatively long timeout because the iframe created by withIframe can
// take a couple seconds to load its JS.
asyncTestCase.stepTimeout = 3 * 1000;

function setUpPage() {
  if (!('Worker' in goog.global)) {
    return;
  }

  // Lengthen the timeout just for this step because the Worker can take a while
  // to start.
  var oldStepTimeout = asyncTestCase.stepTimeout;
  asyncTestCase.stepTimeout = 10 * 1000;

  var worker = new Worker('testdata/portchannel_worker.js');
  workerChannel = new goog.messaging.PortChannel(worker);
  worker.onmessage = function(e) {
    if (e.data == 'loaded') {
      asyncTestCase.continueTesting();
      asyncTestCase.stepTimeout = oldStepTimeout;
    }
  };
  asyncTestCase.waitForAsync('waiting for worker startup');
}

function tearDownPage() {
  goog.dispose(workerChannel);
}

function setUp() {
  timer = new goog.Timer(50);
  mockControl = new goog.testing.MockControl();
  asyncMockControl = new goog.testing.async.MockControl(mockControl);
  mockPort = new goog.events.EventTarget();
  mockPort.postMessage = mockControl.createFunctionMock('postMessage');
  portChannel = new goog.messaging.PortChannel(mockPort);
}

function tearDown() {
  goog.dispose(timer);
  portChannel.dispose();
  mockControl.$verifyAll();
}

function makeMessage(serviceName, payload) {
  var msg = {'serviceName': serviceName, 'payload': payload};
  msg[goog.messaging.PortChannel.FLAG] = true;
  if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) {
    msg = goog.json.serialize(msg);
  }
  return msg;
}

function expectNoMessage() {
  portChannel.registerDefaultService(
    mockControl.createFunctionMock('expectNoMessage'));
}

function receiveMessage(serviceName, payload, opt_origin, opt_ports) {
  mockPort.dispatchEvent(
      goog.testing.messaging.MockMessageEvent.wrap(
          makeMessage(serviceName, payload),
          opt_origin || 'http://google.com',
          undefined, undefined, opt_ports));
}

function receiveNonChannelMessage(data) {
  if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_ &&
      !goog.isString(data)) {
    data = goog.json.serialize(data);
  }
  mockPort.dispatchEvent(
      goog.testing.messaging.MockMessageEvent.wrap(
          data, 'http://google.com'));
}

function testPostMessage() {
  mockPort.postMessage(makeMessage('foobar', 'This is a value'), []);
  mockControl.$replayAll();
  portChannel.send('foobar', 'This is a value');
}

function testPostMessageWithPorts() {
  if (!('MessageChannel' in goog.global)) {
    return;
  }
  var channel = new MessageChannel();
  var port1 = channel.port1;
  var port2 = channel.port2;
  mockPort.postMessage(makeMessage('foobar', {'val': [
    {'_port': {'type': 'real', 'index': 0}},
    {'_port': {'type': 'real', 'index': 1}}
  ]}), [port1, port2]);
  mockControl.$replayAll();
  portChannel.send('foobar', {'val': [port1, port2]});
}

function testReceiveMessage() {
  portChannel.registerService(
      'foobar', asyncMockControl.asyncAssertEquals(
          'testReceiveMessage', 'This is a string'));
  mockControl.$replayAll();
  receiveMessage('foobar', 'This is a string');
}

function testReceiveMessageWithPorts() {
  if (!('MessageChannel' in goog.global)) {
    return;
  }
  var channel = new MessageChannel();
  var port1 = channel.port1;
  var port2 = channel.port2;
  portChannel.registerService(
      'foobar', asyncMockControl.asyncAssertEquals(
          'testReceiveMessage', {'val': [port1, port2]}),
      true);
  mockControl.$replayAll();
  receiveMessage('foobar', {'val': [
    {'_port': {'type': 'real', 'index': 0}},
    {'_port': {'type': 'real', 'index': 1}}
  ]}, null, [port1, port2]);
}

function testReceiveNonChannelMessageWithStringBody() {
  expectNoMessage();
  mockControl.$replayAll();
  receiveNonChannelMessage('Foo bar');
}

function testReceiveNonChannelMessageWithArrayBody() {
  expectNoMessage();
  mockControl.$replayAll();
  receiveNonChannelMessage([5, 'Foo bar']);
}

function testReceiveNonChannelMessageWithNoFlag() {
  expectNoMessage();
  mockControl.$replayAll();
  receiveNonChannelMessage({
    serviceName: 'foobar',
    payload: 'this is a payload'
  });
}

function testReceiveNonChannelMessageWithFalseFlag() {
  expectNoMessage();
  mockControl.$replayAll();
  var body = {
    serviceName: 'foobar',
    payload: 'this is a payload'
  };
  body[goog.messaging.PortChannel.FLAG] = false;
  receiveNonChannelMessage(body);
}

// Integration tests

function testWorker() {
  if (!('Worker' in goog.global)) {
    return;
  }
  workerChannel.registerService('pong', function(msg) {
    assertObjectEquals({'val': 'fizzbang'}, msg);
    asyncTestCase.continueTesting();
  }, true);
  workerChannel.send('ping', {'val': 'fizzbang'});
  asyncTestCase.waitForAsync('worker response');
}

function testWorkerWithPorts() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  var messageChannel = new MessageChannel();
  workerChannel.registerService('pong', function(msg) {
    assertPortsEntangled(msg['port'], messageChannel.port2, function() {
      asyncTestCase.continueTesting();
    });
  }, true);
  workerChannel.send('ping', {'port': messageChannel.port1});
  asyncTestCase.waitForAsync('worker response');
}

function testPort() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  var messageChannel = new MessageChannel();
  workerChannel.send('addPort', messageChannel.port1);
  messageChannel.port2.start();
  var realPortChannel = new goog.messaging.PortChannel(messageChannel.port2);
  realPortChannel.registerService('pong', function(msg) {
    assertObjectEquals({'val': 'fizzbang'}, msg);

    messageChannel.port2.close();
    realPortChannel.dispose();
    asyncTestCase.continueTesting();
  }, true);
  realPortChannel.send('ping', {'val': 'fizzbang'});
  asyncTestCase.waitForAsync('port response');
}

function testPortIgnoresOrigin() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  var messageChannel = new MessageChannel();
  workerChannel.send('addPort', messageChannel.port1);
  messageChannel.port2.start();
  var realPortChannel = new goog.messaging.PortChannel(
      messageChannel.port2, 'http://somewhere-else.com');
  realPortChannel.registerService('pong', function(msg) {
    assertObjectEquals({'val': 'fizzbang'}, msg);

    messageChannel.port2.close();
    realPortChannel.dispose();
    asyncTestCase.continueTesting();
  }, true);
  realPortChannel.send('ping', {'val': 'fizzbang'});
  asyncTestCase.waitForAsync('port response');
}

function testWindow() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }

  withIframe(function() {
    var iframeChannel = goog.messaging.PortChannel.forEmbeddedWindow(
        window.frames['inner'], '*', timer);
    iframeChannel.registerService('pong', function(msg) {
      assertEquals('fizzbang', msg);

      goog.dispose(iframeChannel);
      asyncTestCase.continueTesting();
    });
    iframeChannel.send('ping', 'fizzbang');
    asyncTestCase.waitForAsync('window response');
  });
}

function testWindowCancelled() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  withIframe(function() {
    var iframeChannel = goog.messaging.PortChannel.forEmbeddedWindow(
        window.frames['inner'], '*', timer);
    iframeChannel.cancel();

    iframeChannel.registerService('pong', function(msg) {
      fail('no messages should be received due to cancellation');
      goog.dispose(iframeChannel);
      asyncTestCase.continueTesting();
    });

    iframeChannel.send('ping', 'fizzbang');
    asyncTestCase.waitForAsync('window response');

    // Leave plenty of time for the connection to be made if the test fails, but
    // stop the test before the asyncTestCase timeout is hit.
    setTimeout(goog.bind(asyncTestCase.continueTesting, asyncTestCase),
               asyncTestCase.stepTimeout / 3);
  });
}

function testWindowWontSendToWrongOrigin() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  withIframe(function() {
    var iframeChannel = goog.messaging.PortChannel.forEmbeddedWindow(
        window.frames['inner'], 'http://somewhere-else.com', timer);
    iframeChannel.registerService('pong', function(msg) {
      fail('Should not receive pong from unexpected origin');
      iframeChannel.dispose();
      asyncTestCase.continueTesting();
    });
    iframeChannel.send('ping', 'fizzbang');

    setTimeout(function() {
      iframeChannel.dispose();
      asyncTestCase.continueTesting();
    }, asyncTestCase.stepTimeout - 500);
    asyncTestCase.waitForAsync('window response');
  });
}

function testWindowWontReceiveFromWrongOrigin() {
  if (!('Worker' in goog.global) || !('MessageChannel' in goog.global)) {
    return;
  }
  withIframe(function() {
    var iframeChannel = goog.messaging.PortChannel.forEmbeddedWindow(
        window.frames['inner'], '*', timer);
    iframeChannel.registerService('pong', function(msg) {
      fail('Should not receive pong from unexpected origin');
      iframeChannel.dispose();
      asyncTestCase.continueTesting();
    });
    iframeChannel.send('ping', 'fizzbang');

    setTimeout(function() {
      iframeChannel.dispose();
      asyncTestCase.continueTesting();
    }, asyncTestCase.stepTimeout - 500);
    asyncTestCase.waitForAsync('window response');
  }, 'testdata/portchannel_wrong_origin_inner.html');
}

/**
 * Assert that two HTML5 MessagePorts are entangled by posting messages from
 * each to the other.
 *
 * @param {!MessagePort} port1
 * @param {!MessagePort} port2
 * @param {function()} callback Called when the assertion is finished.
 */
function assertPortsEntangled(port1, port2, callback) {
  port1.onmessage = function(e) {
    assertEquals('port 2 should send messages to port 1',
                 'port2 to port1', e.data);
    callback();
  };

  port2.onmessage = function(e) {
    assertEquals('port 1 should send messages to port 2',
                 'port1 to port2', e.data);
    port2.postMessage('port2 to port1');
    asyncTestCase.waitForAsync('port 1 receiving message');
  };

  port1.postMessage('port1 to port2');
  asyncTestCase.waitForAsync('port 2 receiving message');
}

function withIframe(callback, opt_url) {
  var frameDiv = goog.dom.getElement('frame');
  goog.dom.removeChildren(frameDiv);
  goog.dom.appendChild(frameDiv, goog.dom.createDom('iframe', {
    style: 'display: none',
    name: 'inner',
    id: 'inner',
    src: opt_url || 'testdata/portchannel_inner.html'
  }));

  asyncTestCase.waitForAsync('creating iframe');
  // We need to pass control back to the event loop to give the iframe a chance
  // to load.
  setTimeout(function() {
    asyncTestCase.continueTesting();
    callback();
  }, 0);
}

</script>
</body>
</html>
